package com.bluefox.modules.lock.controller;

import cn.hutool.core.lang.UUID;
import com.bluefox.annotation.AnonymousAccess;
import com.bluefox.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * @author sunfeng
 * @date 2021/05/06 22:24
 **/
@RestController
@RequestMapping("/redisLock")
public class RedisLockController {

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    /**
     * 使用 SETNX 和 DEL 命令组合实现分布锁，存在两个潜在的风险。
     * 第一个风险是，假如某个客户端在执行了 SETNX 命令、加锁之后，紧接着却在操作共享数据时发生了异常，结果一直没有执行最后的 DEL 命令释放锁。
     * ***  解决方法:给锁设置一个过期时间, 即使发生了异常，无法主动释放锁。Redis也会根据所变量的过期时间，在所变量过期后，把它删除。
     * 第二个风险是:如果客户端A执行了SETNX命令加锁后，假设客户端B执行了DEL命令释放锁，此时客户端A的锁就被错误的释放
     * ***  *** 情况1：B先持有锁，超时以后服务端自动释放了锁，A随后获取到了锁，在执行业务过程中，B业务执行完毕，释放锁，此时A还持有锁。
     * ***  *** 解决方案:我们要能去分是来自不同客户端的操作，  我们可以在所变量value上考虑。   将value的值设置为一个UUID 或者当前线程id
     * 在
     */
    @AnonymousAccess
    @GetMapping("testLock")
    public void testLock() {
        //1获取锁，setNx 优化之设置锁的过期时间
//        1. 首先想到通过expire设置过期时间（缺乏原子性：如果在setNx和expire之间出现异常，锁也无法释放）
//        2. 在set时指定过期时间（推荐）
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
        // 问题：可能会释放其他服务器的锁。
        // 场景：如果业务逻辑的执行时间是7s。执行流程如下
        //1.index1业务逻辑没执行完，3秒后锁被自动释放。
        //2.index2获取到锁，执行业务逻辑，3秒后锁被自动释放。
        //3.index3获取到锁，执行业务逻辑
        //4.index1业务逻辑执行完成，开始调用del释放锁，这时释放的是index3的锁，导致index3的业务只执行1s就被别人释放。

        //解决：setnx获取锁时，设置一个指定的唯一值（例如：uuid）；释放前获取这个值，判断是否自己的锁

        //2获取锁成功、查询num的值
        if (lock) {
            Object value = redisTemplate.opsForValue().get("num");
            //2.1判断num为空return
            if (StringUtils.isNull(value)) {
                return;
            }
            int num = Integer.parseInt(value + "");
            redisTemplate.opsForValue().set("num", ++num);
            // 删除操作缺乏原子性
            if (uuid.equals((String) redisTemplate.opsForValue().get("lock"))) {
                redisTemplate.delete("lock");
            }
        } else {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                //3获取锁失败、每隔0.1秒再获取
                testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 针对上面 删除key的操作不是原子性的  我们可以将删除操作放在lua脚本中执行
     */

    @AnonymousAccess
    @GetMapping("testLockLua")
    public void testLockLua() {
        //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
        String uuid = UUID.randomUUID().toString();
        //2 定义一个锁：lua 脚本可以使用同一把锁，来实现删除！
        /**
         * // 访问skuId 为25号的商品 100008348542
         */
        String skuId = "25";
        /**
         * 锁住的是每个商品的数据
         */
        String locKey = "lock:" + skuId;

        // 3 获取锁
        //setIfAbsent如果为空就set值，并返回1
        //如果存在(不为空)不进行操作，并返回0
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
        // 第一种： lock 与过期时间中间不写任何的代码。
        // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
        // 如果true
        if (lock) {
            // 执行的业务逻辑开始
            // 获取缓存中的num 数据
            Object value = redisTemplate.opsForValue().get("num");

            // 如果是空直接返回      此时锁不释放   等待自动释放
            if (StringUtils.isNull(value)) {
                return;
            }
            // 不是空 如果说在这出现了异常！ 那么delete 就删除失败！ 也就是说锁永远存在！
            int num = Integer.parseInt(value + "");
            // 使num 每次+1 放入缓存
            redisTemplate.opsForValue().set("num", String.valueOf(++num));
            /*使用lua脚本来解锁*/
            // 定义lua 脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then" +
                    "   return redis.call('del', KEYS[1]) " +
                    "else " +
                    "   return 0 " +
                    "end";
            // 使用redis执行lua执行
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            // 设置一下返回值类型 为Long
            // 因为删除判断的时候，返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型，
            // 那么返回字符串与0 会有发生错误。
            redisScript.setResultType(Long.class);
            // 第一个要是script 脚本 ，第二个需要判断的key，第三个就是key所对应的值。
            redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);


        } else {
            // 其他线程等待
            try {
                // 睡眠
                Thread.sleep(1000);
                // 睡醒了之后，调用方法。
                testLockLua();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
